package com.obixlabs.security.credentialsstore.server.dao.ldapimpl;

import static com.obixlabs.security.credentialsstore.server.dao.ldapimpl.LDAPConstants.COMMON_NAME_ATTRIBUTE;
import static com.obixlabs.security.credentialsstore.server.dao.ldapimpl.LDAPConstants.DESCRIPTION_ATTRIBUTE;
import static com.obixlabs.security.credentialsstore.server.dao.ldapimpl.LDAPConstants.OBJECT_CLASS_ATTRIBUTE;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.naming.NameAlreadyBoundException;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.AttributeInUseException;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.NoSuchAttributeException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.obixlabs.commons.ExceptionUtil;
import com.obixlabs.security.credentialsstore.exception.EntryNotFoundException;
import com.obixlabs.security.credentialsstore.exception.SecurityRuntimeException;
import com.obixlabs.security.credentialsstore.model.BaseUser;
import com.obixlabs.security.credentialsstore.model.GroupPermissions;
import com.obixlabs.security.credentialsstore.model.ObixGroup;
import com.obixlabs.security.credentialsstore.model.ObixGroups;
import com.obixlabs.security.credentialsstore.model.ObixPermission;
import com.obixlabs.security.credentialsstore.model.ObixPermissions;
import com.obixlabs.security.credentialsstore.server.dao.GroupDAO;

public class GroupDAOImpl extends AbstractLDAPDAO implements GroupDAO 
{
	private static final Logger logger = 
		LoggerFactory.getLogger(GroupDAOImpl.class);
	
	private static final String GROUP_OF_UNIQUE_NAMES_OBJ_CLASS = "groupOfUniqueNames";
	private static final String[] GROUP_OBJECT_CLASSES = {	"top",GROUP_OF_UNIQUE_NAMES_OBJ_CLASS};
	private static final String UNIQUE_MEMBER_ATTR_GROUP_NAME = "uniqueMember";
	
	private PermissionDAOImpl permissionDAO;	
	
	public PermissionDAOImpl getPermissionDAO() {return permissionDAO;}
	public void setPermissionDAO(PermissionDAOImpl permissionsDAO) 
	{this.permissionDAO = permissionsDAO;}
	
	@Override
	public void delete(ObixGroup group) 
	{
        String groupDN = 
        	LDAPAttributesFactory.getGroupDN(	group,
        										getGroupCategorizationConfig()); 
		try
		{
			ObixPermissions permissions = 
				getPermissionDAO().findByGroup(group);
			for (ObixPermission obixPermission : permissions.values())
					getPermissionDAO().revokePermission(group, obixPermission);
			
			//delete permission as final step
			getAdminConnection().getContext().destroySubcontext(groupDN);
		}
		catch (NameNotFoundException exception)
		{
			String errorMessage = 
					"Group (" + groupDN
					+ ") does not exist. Delete request will be ignored.";
			logger.warn(errorMessage, exception);
		}
		catch (NamingException exception)
		{
			throw new SecurityRuntimeException(
					"Could not delete group from LDAP directory due "
							+ "to unexpected exception.\n\tStack Trace:\n"
							+ ExceptionUtil.getStackTrace(exception));
		}				
	}
	
	@Override
	public ObixGroup findByName(String name) 
	{
        ObixGroup result;
        
        try
        {
	        // Set up criteria to search on
        	StringBuffer filter = new StringBuffer();
        
	        filter = filter	.append("(&")
							.append("(objectClass=" +
									GROUP_OF_UNIQUE_NAMES_OBJ_CLASS+")")
							.append("("+COMMON_NAME_ATTRIBUTE+"=")        				
							.append(name)
							.append(")")
							.append(")");
	
	        // Set up search constraints
	        SearchControls cons = new SearchControls();
	        cons.setSearchScope(SearchControls.ONELEVEL_SCOPE);
	
	        NamingEnumeration<SearchResult> results =
	            getAdminConnection().getContext().search(
	            			getCategorizationInfo().getGroupCategorizationConfig().getGroupsBaseDN(), 
	            			filter.toString(),cons);
	        
	        SearchResult searchResult;
	        if (results.hasMore()) 
	        {
	        	searchResult = results.next();
	        	result = populateInstanceFromAttributes(searchResult.getAttributes());
	        }
	        else result = null; 
        }
        catch (NamingException exception)
        {
        	throw new SecurityRuntimeException(	"Could not search LDAP directory for group '" + 
        										name + "' due " +"to unexpected exception. Stack Trace:\n"+
												ExceptionUtil.getStackTrace(exception));        	
        }
        return result;
	}

	
	@Override
	public ObixGroups findByUser(BaseUser user) 
	{
        ObixGroups result = new ObixGroups();
        
        try
        {
	        // Set up criteria to search on
        	StringBuffer filter = new StringBuffer();
        
	        //if a user is specified, then also 
	        //filter by the userid
	        if (user!=null)
	        {
		        filter = filter	.append("(&")
								.append("(objectClass=" +
										GROUP_OF_UNIQUE_NAMES_OBJ_CLASS+")")
								.append("("+UNIQUE_MEMBER_ATTR_GROUP_NAME+"=")        				
	            				.append(LDAPAttributesFactory.getUserDN(user, 
	            						getUserCategorizationConfig()))
	            				.append(")")
	            				.append(")");
	        }
	        else filter = filter.append("(objectClass=" + GROUP_OF_UNIQUE_NAMES_OBJ_CLASS+")");
	        
	        		        
	
	        // Set up search constraints
	        SearchControls cons = new SearchControls();
	        cons.setSearchScope(SearchControls.ONELEVEL_SCOPE);
	
	        NamingEnumeration<SearchResult> results =
	            getAdminConnection().getContext().search(
	            			getCategorizationInfo().getGroupCategorizationConfig().getGroupsBaseDN(), 
	            			filter.toString(), cons);
	        
	        SearchResult searchResult;
	        ObixGroup group;
	        while (results.hasMore()) 
	        {
	        	searchResult = results.next();
	        	group = populateInstanceFromAttributes(searchResult.getAttributes());
	            result.put(group.getName(), group);
	        }
        }
        catch (NamingException exception)
        {
        	throw new SecurityRuntimeException(	"Could not search LDAP directory due " +
												"to unexpected exception. Stack Trace:\n"+
												ExceptionUtil.getStackTrace(exception));        	
        }
        return result;
	}
		
	@Override
	public ObixGroups findAll() { return findByUser(null); }

	@Override
	public ObixGroups findByPermission(ObixPermission permission) throws EntryNotFoundException
	{ return getPermissionDAO().getGroupsAssignedTo(permission, this); }

	@Override
	public ObixGroup save(ObixGroup group) 
	{
		ObixGroup existing = findByName(group.getName());
		if (existing!=null)
			internalUpdateOld(existing, group);
		else internalSaveNew(group);
		
		refreshPermissions(group);
		return group;
	}

	ObixGroup constructFromDN(String groupDN,
								LDAPConnection connection) 
								throws NamingException 
	{
		ObixGroup result;
		Attributes attributes = 
			connection.getContext().getAttributes(groupDN);
		result = populateInstanceFromAttributes(attributes);
		return result;
	}
	
	
	List<String> getGroupMemberDNs(ObixGroup group) throws EntryNotFoundException
	{
		List<String> result = new ArrayList<String>();
		try
		{
			String groupDN = LDAPAttributesFactory.getGroupDN(	group,
					getGroupCategorizationConfig()); 
			Attributes groupAttributes= 
				getAdminConnection().getContext().getAttributes(groupDN);
			if (groupAttributes!=null && groupAttributes.size()>0)
			{			
				Attribute members = groupAttributes.get(UNIQUE_MEMBER_ATTR_GROUP_NAME);
		        NamingEnumeration<?> memberDNs=members.getAll();
		        
		        String userDN;
		        while(memberDNs.hasMore())
		        {
		        	userDN = memberDNs.next().toString();
		        	result.add(userDN);
		        }
			}
		}
		catch (NameNotFoundException nameNotFound) 
		{
			throw new EntryNotFoundException("Group '" + group + "' not found in directory." +
					"Naming error details: \n\t" + ExceptionUtil.getStackTrace(nameNotFound));
		}
        catch (NamingException exce)
        {
        	throw new SecurityRuntimeException(	"Could not extract group members due " +
        										"to unexpected naming exception.\n\tStack Trace:\n"+
												ExceptionUtil.getStackTrace(exce));
        }        
        return result;
	}	
	
	void addUserToGroup(ObixGroup group, BaseUser user)
	{
        String userDN = LDAPAttributesFactory.getUserDN(user, getUserCategorizationConfig());
        
        String groupDN = LDAPAttributesFactory.getGroupDN(	group, 
															getGroupCategorizationConfig()); 


		try 
        {
            ModificationItem[] modifications = new ModificationItem[1];

            
            Attribute modification =
                new BasicAttribute(GroupDAOImpl.UNIQUE_MEMBER_ATTR_GROUP_NAME,
                                   userDN);
            modifications[0] =
                new ModificationItem(DirContext.ADD_ATTRIBUTE, modification);
            
            getAdminConnection().getContext().modifyAttributes(groupDN, modifications);
        } 
        catch (AttributeInUseException exception) 
        {
        	String errorMessage = 	"User (" +userDN + ") is already a member of " +
        							"the Group ("+groupDN+"). Request will be ignored!";
        	logger.warn(errorMessage,exception);    	
        }
		catch (NamingException namingException)
		{
        	throw new SecurityRuntimeException(	"Could not assign user to group due " +
												"to unexpected exception. \n\tException Stack Trace:\n"+
												ExceptionUtil.getStackTrace(namingException));			
		}				
	}
	
	void removeUserFromGroup(ObixGroup group, BaseUser user)
	{
        String userDN = LDAPAttributesFactory.getUserDN(user,getUserCategorizationConfig());
        
        String groupDN = LDAPAttributesFactory.getGroupDN(group, 
											getGroupCategorizationConfig()); 
        
        try 
        {
            ModificationItem[] modifications = new ModificationItem[1];

            Attribute modification =
                new BasicAttribute(	GroupDAOImpl.UNIQUE_MEMBER_ATTR_GROUP_NAME,
                					userDN);
            
            modifications[0] =
                new ModificationItem(DirContext.REMOVE_ATTRIBUTE, modification);
            getAdminConnection().getContext().modifyAttributes(	groupDN, 
            													modifications);
        } 
        catch (NoSuchAttributeException noSuchAttributeException) 
        {
        	String errorMessage = 	"User (" +userDN + ") is not a member of the " +
									"Group ("+groupDN+"). Request will be ignored!";
        	logger.warn(errorMessage, noSuchAttributeException);
        }
		catch (NamingException namingException)
		{
        	String errorMessage = 	"User (" +userDN + ") cannot be removed from " +
									"Group ("+groupDN+") due to unexpected LDAP error. " +
									"\n\tException Stack Trace:\n"+
									ExceptionUtil.getStackTrace(namingException);        	
        	throw new SecurityRuntimeException(errorMessage);
		}		
	}
	
	private void internalUpdateOld(	ObixGroup oldState, 
									ObixGroup newState)
	{
        String groupDN = LDAPAttributesFactory.getGroupDN(oldState, 
				getGroupCategorizationConfig()); 
		try 
		{
			ModificationItem[] modifications = new ModificationItem[1];

	        Attribute desc = new BasicAttribute(DESCRIPTION_ATTRIBUTE, 
												newState.getDescription());

			modifications[0] = new ModificationItem(
								DirContext.REPLACE_ATTRIBUTE, desc);

			getAdminConnection().getContext().modifyAttributes(
											groupDN, modifications);
			
			internalUpdatePermissions(newState);
		} 
		catch (NamingException exception) 
		{
			String errorMessage = "Unexpected Error: Could not update group ("
					+ groupDN + "). Possible LDAP integrity error." + 
					"\n\tStack Trace:\n"
					+ ExceptionUtil.getStackTrace(exception);
			throw new SecurityRuntimeException(errorMessage);
		}				
	}
	
	private void internalSaveNew(ObixGroup group)
	{
        // Create a container set of attributes
        Attributes container = new BasicAttributes();

        // Create the objectclass to add
        Attribute objClasses = new BasicAttribute(OBJECT_CLASS_ATTRIBUTE);
        for (String objectClass : GroupDAOImpl.GROUP_OBJECT_CLASSES)
        	objClasses.add(objectClass);

        // Assign the name and description to the group
        Attribute cn = new BasicAttribute(COMMON_NAME_ATTRIBUTE, 
        									group.getName());
        Attribute desc = new BasicAttribute(DESCRIPTION_ATTRIBUTE, 
        									group.getDescription());

                
        // Add these to the container
        container.put(objClasses);
        container.put(cn);
        container.put(desc);
        
        //add administrator to the group
        //principal is the same as the user DN
        Attribute uniqueMemberAttr =
            new BasicAttribute(GroupDAOImpl.UNIQUE_MEMBER_ATTR_GROUP_NAME,getAdminConnection().getPrincipalDN());              
        container.put(uniqueMemberAttr);        
        
        LDAPAttributesFactory.addOrgIdentificationAttributes(	container, 
        														getGroupCategorizationConfig());
        

        String groupDN = LDAPAttributesFactory.getGroupDN(	group, 
        													getGroupCategorizationConfig()); 
        try
        {
        	//Create the entry
        	getAdminConnection().getContext().
        				createSubcontext(groupDN,container);

        	internalUpdatePermissions(group);
        }
        catch (NameAlreadyBoundException userAlreayExists)
        {
        	throw new SecurityRuntimeException(	"Group (" + groupDN + ") already exists " +
        										"in the Obix directory. Possible application " +
        										"or concurrency error. \n\tStack Trace:\n"+
        										ExceptionUtil.getStackTrace(userAlreayExists));
        }
        catch (NamingException exce)
        {
        	throw new SecurityRuntimeException(	"Could not persist group to LDAP directory due " +
        										"to unexpected exception.\n\tStack Trace:\n"+
												ExceptionUtil.getStackTrace(exce));
        }		
	}
	
	private void internalUpdatePermissions(ObixGroup group)
	{		
		if (group.getPermissions()==null) return;
		
		//update revoked permissions
		Set<ObixPermission> revokedPermissions = 
			group.getPermissions().getRevokedPermissions();
		for (ObixPermission obixPermission : revokedPermissions)
			getPermissionDAO().revokePermission(group, obixPermission);
		
		//then add new ones -- this sequence matters in 
		//case a permission is revoked and re-assigned 
		//in the same request
		Set<ObixPermission> assignedPermissions = 
			group.getPermissions().getNewlyAssignedPermissions();
		for (ObixPermission obixPermission : assignedPermissions)
			getPermissionDAO().assignPermission(group, obixPermission);
	}
			    
    private void refreshPermissions(ObixGroup group)
    {
		ObixPermissions permissions =
			getPermissionDAO().findByGroup(group);
			
		group.setPermissions(new GroupPermissions(permissions));    	
    }
	
	private ObixGroup populateInstanceFromAttributes(
										Attributes groupAttributes) 
										throws NamingException
	{
		ObixGroup result = new ObixGroup();
		
		Attribute commonName = groupAttributes.get(COMMON_NAME_ATTRIBUTE);
		Attribute groupDescription = groupAttributes.get(DESCRIPTION_ATTRIBUTE);
		
		result.setName(commonName.get().toString());
		if (groupDescription!=null && groupDescription.get()!=null)
			result.setDescription(groupDescription.get().toString());
		
		refreshPermissions(result);
		
		return result;
	}	

}